package erfgame.core.util;

import java.awt.Color;


import erfgame.core.Point3D;
import erfgame.core.world.terrain.TerrainColorSource;
import erfgame.core.world.terrain.TerrainConstants;

public class ColorUtils {

	public static final int PRECISION = 12;
	private static final int ONE = 1 << PRECISION;
	private static final double TWO_ROOT_HALF = 2 * Math.sqrt( 0.5 );
	
	public static final int ROTATION_LENGTH_MINUS_1 = 3;
	public static final int ROTATION_MASK = ROTATION_LENGTH_MINUS_1;
	
	public static final int YSPACING_SHIFT = 1;
	public static final int YSPACING_MASK = 1;
	public static final int YSPACING = 1 << YSPACING_SHIFT;
	public static final int MAX_DIFF = 16;// 1 << MAX_DIFF_SHIFT;
	public static final int MAX_DIFF_MINUS_1 = MAX_DIFF - 1;
	//public static final int MAX_DIFF_FP = MathFP.toFP( MAX_DIFF );
	public static final int MAX_DIFF_EXTENT = MAX_DIFF * 2;
	
	private static final int DIVISOR_SHIFT = PRECISION;
//	private static final int DIVISOR = 1 << DIVISOR_SHIFT;
	
//	private static final int PI_ON_4 = MathFP.PI / 4;
	
	public static final Color TRANSPARENT = new Color(0, 0, 0, 0);
	
//	private static final int[] SIN_VALUES = new int[ MathFP.PI * 2+1 ];
	//private static final int[] COS_VALUES = new int[ MathFP.PI * 2+1 ];
	private static final int [][] ATAN2_VALUES = new int[MAX_DIFF_EXTENT*2 + 1][MAX_DIFF_EXTENT*2 + 1];
	
	public static final Point3D[] YZ_LEFT_HANDED = new Point3D[]{new Point3D(0, -1, 0),new Point3D(0, 0, 1),new Point3D(0, 1, 0),new Point3D(0, 0, -1)};
	public static final Point3D[] YZ_RIGHT_HANDED = new Point3D[]{new Point3D(0, -1, 0),new Point3D(0, 0, -1),new Point3D(0, 1, 0),new Point3D(0, 0, 1)};

	public static final Point3D[] XZ_LEFT_HANDED = new Point3D[]{new Point3D(-1, 0, 0),new Point3D(0, 0, 1),new Point3D(1, 0, 0),new Point3D(0, 0, -1)};
	public static final Point3D[] XZ_RIGHT_HANDED = new Point3D[]{new Point3D(-1, 0, 0),new Point3D(0, 0, -1),new Point3D(1, 0, 0),new Point3D(0, 0, 1)};

	public static final Point3D[] XY_LEFT_HANDED = new Point3D[]{new Point3D(-1, 0, 0),new Point3D(0, 1, 0),new Point3D(1, 0, 0),new Point3D(0, -1, 0)};
	public static final Point3D[] XY_RIGHT_HANDED = new Point3D[]{new Point3D(-1, 0, 0),new Point3D(0, -1, 0),new Point3D(1, 0, 0),new Point3D(0, 1, 0)};

	
	static {
		calculateTrigValues();
	}

	private Point3D xyResult;
	private Point3D xzResult;
	private Point3D yzResult;

	public ColorUtils() {
		this.xyResult = new Point3D();
		this.yzResult = new Point3D();
		this.xzResult = new Point3D();
	}
	
	public static final int toGray( int color ) {
		return averageColors( 0x000000, 1, color, 3 );
	}

	public static void calculateTrigValues() {
		
		int bottom = MAX_DIFF_EXTENT;
//		int minRange = Integer.MAX_VALUE;
//		int maxRange = Integer.MIN_VALUE;
		for( int bottomIndex = MAX_DIFF_EXTENT*2; bottomIndex>=0; bottomIndex-- ) {
			int[] atan2ValuesBottom = ATAN2_VALUES[bottomIndex]; 
			int top = MAX_DIFF_EXTENT;
			for( int topIndex = MAX_DIFF_EXTENT*2; topIndex>=0; topIndex-- ) {
				if(top != 0 || bottom != 0) {
//					try {
					double atan2 = Math.atan2(top, bottom);
//					if( atan2 > maxRange ) {
//						maxRange = atan2;
//					}
//					if(  atan2 < minRange ) {
//						minRange = atan2;
//					}
					int precisionMultiplier = 1  << PRECISION; 
					double a = Math.sin(atan2+Math.PI/6) * precisionMultiplier;
//					double b = Math.sin(atan2) * precisionMultiplier;
					
//					atan2ValuesBottom[topIndex] = (int)Math.round(b) << 16 | (0xFFFF & (int)Math.round(a));
					atan2ValuesBottom[topIndex] = (int)Math.round(a);
//					} catch( Exception ex ) {
//						System.err.println( "unable to set MathFP.atan2("+top+","+bottom+")" );
//						ex.printStackTrace();
//					}
				}
				top--;
			}
			bottom--;
		}
//		System.out.println( MathFP.toString(minRange) +" -> "+MathFP.toString(maxRange) );
	}
	
//	public static final int sin( int v ) {
//		return SIN_VALUES[ (int)(v + MathFP.PI) ];
//	}

//	public static final int cos( int v ) {
//		return COS_VALUES[ (int)(v + MathFP.PI) ];
//	}
	
	public static final int atan2sin( int a, int b ) {
		return ATAN2_VALUES[b + MAX_DIFF_EXTENT][a + MAX_DIFF_EXTENT];
	}

	public static final int invert( int c ) {
		int inv = 0xFFFFFF & ~c;
		return inv;
	}
	
	public static final int averageColors( int c1, int c2 ) {
		int b1 = (0x0000FF & c1);
		int g1 = (0x00FF00 & c1);
		int r1 = (0xFF0000 & c1);
		int b2 = (0x0000FF & c2);
		int g2 = (0x00FF00 & c2);
		int r2 = (0xFF0000 & c2);
		return 0xFF000000 | ( ((b1+b2)/2) & 0x0000FF ) | ( ((g1+g2)/2) & 0x00FF00 ) | ( ((r1+r2)/2) & 0xFF0000 );
	}

	public static final int averageColors( int c1, int w1, int c2, int w2 ) {
		int wtotal = w1 + w2;
		int b1 = (0x0000FF & c1);
		int g1 = (0x00FF00 & c1);
		int r1 = (0xFF0000 & c1);
		int b2 = (0x0000FF & c2);
		int g2 = (0x00FF00 & c2);
		int r2 = (0xFF0000 & c2);
		return 0xFF000000 | ( ((b1*w1+b2*w2)/wtotal) & 0x0000FF ) | ( ((g1*w1+g2*w2)/wtotal) & 0x00FF00 ) | ( ((r1*w1+r2*w2)/wtotal) & 0xFF0000 );
	}

	public static final int multiplyColor( int baseColor, int minColor, int maxColor, int mult, int divShift ) {
		if( mult < 0 ) {
			return multiplyColor( baseColor, minColor, -mult, divShift );
		} else {
			return multiplyColor( baseColor, maxColor, mult, divShift );
		}
	}
	
	public static final int multiplyColor( int baseColor, int maxColor, int mult, int divShift ) { 
		// TODO : ints are probably faster than longs
		long b = (0x0000FF & baseColor);
		long g = (0x00FF00 & baseColor);
		long r = (0xFF0000 & baseColor);
		long maxr = 0xFF0000 & maxColor;
		long maxg = 0x00FF00 & maxColor;
		long maxb = 0x0000FF & maxColor;
		// average
		b = b + (((maxb - b) * mult) >> divShift);
		g = g + (((maxg - g) * mult) >> divShift);
		r = r + (((maxr - r) * mult) >> divShift);

		//TODO: alpha?
		return (int)(0xFF000000 | (r & 0xFF0000) | (g & 0x00FF00) | b);		
	}

	public static final int multiplyColorUsingDiv( int baseColor, int maxColor, int mult, int div ) { 
		// TODO : ints are probably faster than longs
		long b = (0x0000FF & baseColor);
		long g = (0x00FF00 & baseColor);
		long r = (0xFF0000 & baseColor);
		long maxr = 0xFF0000 & maxColor;
		long maxg = 0x00FF00 & maxColor;
		long maxb = 0x0000FF & maxColor;
		// average
		b = b + (((maxb - b) * mult) / div);
		g = g + (((maxg - g) * mult) / div);
		r = r + (((maxr - r) * mult) / div);

		//TODO: alpha?
		return (int)(0xFF000000 | (r & 0xFF0000) | (g & 0x00FF00) | b);		
	}	

	public static final boolean getPathDeltas( 
			int cx, int cy, int cz,
			byte[][][][][][] terrain, 
			int x, int y, int z, 
			int width, int height, int depth, 
			Point3D[] leftRotation, Point3D[] rightRotation,
			boolean allowCurves,
			Point3D result, 
			VerticalEdge firstVerticalEdge,
			boolean clearRoof ) {
		int upStepsRemaining = getPathDeltas( 
				cx, cy, cz, 
				terrain, 
				x, y, z, 
				x, y, z,
				width, height, depth, 
				rightRotation, 
				allowCurves, 
				result, 
				firstVerticalEdge,
				clearRoof
		);
		int upx = result.x;
		int upy = result.y;
		int upz = result.z;

		int downStepsRemaining = getPathDeltas( 
				cx, cy, cz, 
				terrain, 
				x, y, z, 
				x, y, z,
				width, height, depth, 
				leftRotation, 
				allowCurves, 
				result, 
				firstVerticalEdge,
				clearRoof
		);

		int downx = result.x;
		int downy = result.y;
		int downz = result.z;		
		
		// don't normalize? 
		result.x = upx - downx;
		result.y = upy - downy;
		result.z = upz - downz;
		return upStepsRemaining < MAX_DIFF_MINUS_1 || downStepsRemaining < MAX_DIFF_MINUS_1;
	}
	
	public static final int getPathDeltas(
		int cx, int cy, int cz,
		byte[][][][][][] terrain, 
		int x, int y, int z, 
		int ax, int ay, int az,
		int width, int height, int depth, 
		Point3D[] offsetOrder,
		boolean allowCurves,
		Point3D deltas, 
		VerticalEdge edge,
		boolean clearRoof
	) 
	{
		int previousTurnX = ax;
		int previousTurnY = ay;
		int previousTurnZ = az;
		
		int primaryIndex = -1;
		int secondaryIndex = -1;
		int previousIndex = -1;
		if( offsetOrder == null ) {
			return 0;
		}
		int deltaX = 0;
		int deltaY = 0;
		int deltaZ = 0;
		boolean done = false;
		byte[][][] cterrain = terrain[cx][cy][cz];
		int pathLength = MAX_DIFF; 
		int turn = 0;
		outer: while( !done ) {
			pathLength--;
			if( pathLength > 0 ) {
				int previousConsecutiveAir = 0;
				for( int i=0; i<=offsetOrder.length; i++ ) {
					int index = (i+previousIndex+ROTATION_LENGTH_MINUS_1) & ROTATION_MASK;
					Point3D offset = offsetOrder[index];
					int dx = offset.x;
					int dy = offset.y;
					int dz = offset.z;
					byte[][][] ctterrain = cterrain;
					int ctx = cx;
					int cty = cy;
					int ctz = cz;
					int tx = x + dx;
					int ty = y + dy;
					int tz = z + dz;
					// move to the next block
					if( tx < 0 ) {
						if( ctx > 0 ) {
							ctx--;
							tx += width;
							ctterrain = terrain[ctx][cty][ctz];
						} else {
							ctterrain = null;
						}
					} else if( tx >= width ) {
						ctx++;
						if( ctx < terrain.length ) {
							tx = tx -= width;
							ctterrain = terrain[ctx][cty][ctz];
						} else {
							ctterrain = null;
						}
					}
					if( ctterrain != null ) {
						if( ty < 0 ) {
							if( cty > 0 ) {
								cty--;
								ty += height;
								ctterrain = terrain[ctx][cty][ctz];
							} else {
								ctterrain = null;
							}
						} else if( ty >= height ) {
							cty++;
							if( cty < terrain[ctx].length ) {
								ty = ty -= height;
								ctterrain = terrain[ctx][cty][ctz];
							} else {
								ctterrain = null;
							}
						}
						if( ctterrain != null ) {
							if( tz < 0 ) {
								if( ctz > 0 ) {
									ctz--;
									tz += depth;
									ctterrain = terrain[ctx][cty][ctz];						
								} else {
									ctterrain = null;
								}
							} else if( tz >= depth ) {
								ctz++;
								if( ctz < terrain[ctx][cty].length ) {
									tz = tz -= depth;
									ctterrain = terrain[ctx][cty][ctz];
								} else {
									ctterrain = null;
								}
							}
						}
					}
					boolean valid = ctterrain != null || (clearRoof && az == 0);
					if( valid ) {
						boolean isSolid;
						if( ctterrain != null ) {
							byte terrainType = ctterrain[ tx ][ ty ][ tz ];
							isSolid = terrainType != TerrainConstants.TERRAIN_TYPE_AIR;
						} else {
							isSolid = false;
						}
						if( edge != null  && !isSolid && dz==0 && edge.remainingLength < pathLength ) {
							edge.cx = cx;
							edge.cy = cy;
							edge.cz = cz;
							edge.x = x;
							edge.y = y;
							edge.z = z;
							
							edge.remainingLength = pathLength;
						}
						if( previousConsecutiveAir > 0 && isSolid || isSolid && i==offsetOrder.length && previousIndex >= 0 ) {
							int atx = ax + dx;
							int aty = ay + dy;
							int atz = az + dz;
							if( previousIndex < 0 || index == previousIndex ) {
								if( primaryIndex < 0 ) {
									primaryIndex = index;
								} else if( !allowCurves && primaryIndex != index ) {
									// drop out, we've hit a major turn (more than one pixel in depth against the current direction)
									if( secondaryIndex >= 0 && pathLength == MAX_DIFF-3 && index == secondaryIndex ) {
										// it's possible we've got the primary and secondary indexes mixed up if we started on a corner
										int tempIndex = secondaryIndex;
										secondaryIndex = primaryIndex;
										primaryIndex = tempIndex;
									} else {
										break outer;
									}									
								}
								// keep moving in a straight line
//								int subPathCount = getStraightPath(
//										ctx, cty, ctz,
//										terrain, 
//										tx, ty, tz,
//										atx, aty, atz,
//										width, height, depth, 
//										offsetOrder, path, 
//										index, pathLength 
//								);
//								pathLength = subPathCount;
								// pathLength / maxPathLength
//								deltaX += (atx - ax);
//								deltaY += (aty - ay);
//								deltaZ += (atz - az);
								cx = ctx;
								cy = cty;
								cz = ctz;
								ax = atx;
								ay = aty;
								az = atz;
								previousIndex = index;
								x = tx;
								y = ty;
								z = tz;
								cterrain = ctterrain;
								continue outer;
							} else {
								if( index == ((primaryIndex + 2) & 3) ) {
									// reversed primary index, also time to exit
									break outer;
								} else if( secondaryIndex < 0 ) { 
									secondaryIndex = index;
								} else if( !allowCurves && secondaryIndex != index && primaryIndex != index ) {
									// change in angle
									break outer;
								}
								// we've hit a turn, add the turn direction see if we've maxed out
								deltaX += (ax - previousTurnX);
								deltaY += (ay - previousTurnY);
								deltaZ += (az - previousTurnZ);
								previousTurnX = ax;
								previousTurnY = ay;
								previousTurnZ = az;
								turn++;
								cx = ctx;
								cy = cty;
								cz = ctz;
								ax = atx;
								ay = aty;
								az = atz;
								previousIndex = index;
								x = tx;
								y = ty;
								z = tz;
								cterrain = ctterrain;
								continue outer;									
							}
						} else {
							if( isSolid ) {
								previousConsecutiveAir = 0;
							} else {
								previousConsecutiveAir++;
							}
						}
					} else {
						// TODO : there may be better ways to get this
						previousConsecutiveAir = 0;
					}
				}
			}
			done = true;
			// has naturally ended, we should add on the deltas
			deltaX += (ax - previousTurnX);
			deltaY += (ay - previousTurnY);
			deltaZ += (az - previousTurnZ);			
		}
		deltas.x = deltaX;// >> PRECISION;
		deltas.y = deltaY;// >> PRECISION;
		deltas.z = deltaZ;// >> PRECISION;
		return pathLength;
	}
	
	public static class VerticalEdge {
		private int cx;
		private int cy;
		private int cz;
		private int x;
		private int y;
		private int z;
		
		private int remainingLength;	
		
		public String toString() {
			return getClass().getSimpleName() + "{cx="+cx+" cy="+cy+" cz="+cz+" x="+x+" y="+y+" z="+z+" remainingLength="+remainingLength+"}";
		}
	}
}
